/**
 * Copyright (C) 2008 Christopher Allen Ogden
 *
 *     This file is part of Odysi.
 *
 *  Odysi is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  Odysi is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with Odysi.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "gameObject.h"
#include "map.h"
#include "animationListManager.h"
#include "postService.h"
#include "CEGUISupport.h"
#include "luaState.h"
#include "stateMachine.h"
#include "clientLua.h"
#include "luaAux.h"
#include "util.h"
#include "character.h"

#define INIT_SCRIPT "/scripts/client.init.lua"
#define CONFIG_VAL_PORT "port"
#define CONFIG_VAL_HOST "host"
#define LAYOUT_LOGIN "login.layout"
#define PLAYER_ID "playerID"

using std::string;
using std::runtime_error;
using std::cout;
using std::endl;
using std::cerr;
using std::stringstream;

namespace {
    /**
     * Render the map.
     *
     * @param map   Map.
     * @param id    ID of player.
     */
    void renderMap(Map &map, int id)
    {
        // Draw the ground tiles.
        Character *chr = dynamic_cast<Character *>(map.getDynamicObject(id));
        if (!chr) return;

        static const int tilesPerWidth = SCREEN_WIDTH / TILE_SIZE;
        static const int tilesPerHeight = SCREEN_HEIGHT / TILE_SIZE;
        Location loc(chr->getLocation());
        int extraTiles = loc.getZ() / TILE_SIZE + 1;
        map.draw(
                loc.getX() - tilesPerWidth / 2 - 3,
                loc.getY() - tilesPerHeight / 2 - extraTiles,
                loc.getX() + tilesPerWidth / 2 + 3,
                loc.getY() + tilesPerHeight / 2 + 9,
                static_cast<int>(-loc.getXOff() - TILE_SIZE / 4 - TILE_SIZE * 3),
                static_cast<int>(-loc.getYOff() - TILE_SIZE / 4 - TILE_SIZE * extraTiles) + loc.getZ());
    }

    /**
     * Renders a frame.
     */
    void render()
    {
        // Render the screen.
        RenderSystem::GS().render();

        // Render interface.
        CEGUI::System::getSingleton().renderGUI();

        // Swap the buffers.
        SDL_GL_SwapBuffers();
    }

    /**
     * Loads all the tiles.
     */
    void loadTiles(void)
    {
        TileManager::GS().load(LEVEL_BASE, PKGDATADIR FILE_TILEMAP_BASE);
        TileManager::GS().load(LEVEL_OBJECTS, PKGDATADIR FILE_TILEMAP_OBJECTS);
        TileManager::GS().load(LEVEL_CHARACTERS, PKGDATADIR FILE_TILEMAP_CHARACTERS);
    }

    /**
     * Handles character movement.
     *
     * @param keystate  An array of keys.
     * @param map       Global map.
     * @param id        ID of player.
     * @param L         Lua state.
     */
    void handleMovement(Uint8 *keystate, Map &map, int id, lua_State *L)
    {
        Character *chr = dynamic_cast<Character *>(map.getDynamicObject(id));
        if (!chr) return;

        if(chr->isMoving()) return;

        // Get the X and Y location of the character.
        Location loc(chr->getLocation());
        int x = loc.getX();
        int y = loc.getY();
        int z = loc.getZ();
        int h = chr->getHeight();

        // Get the keystats.
        bool up = keystate[SDLK_UP];
        bool down = keystate[SDLK_DOWN];
        bool left = keystate[SDLK_LEFT];
        bool right = keystate[SDLK_RIGHT];

        // Get the offset.
        int xOffset = 0;
        int yOffset = 0;
        int newZ = 0;
        int dir = 0;
        if (up && left && map.canWalk(x, y, x - 1, y - 1, z, h)) {
            xOffset--;
            yOffset--;
            dir = 0;
            newZ = map.getBlockedHeight(x - 1, y - 1, z, h);
        } else if (up && right && map.canWalk(x, y, x + 1, y - 1, z, h)) {
            xOffset++;
            yOffset--;
            dir = 1;
            newZ = map.getBlockedHeight(x + 1, y - 1, z, h);
        } else if (down && left && map.canWalk(x, y, x - 1, y + 1, z, h)) {
            xOffset--;
            yOffset++;
            dir = 2;
            newZ = map.getBlockedHeight(x - 1, y + 1, z, h);
        } else if (down && right && map.canWalk(x, y, x + 1, y + 1, z, h)) {
            xOffset++;
            yOffset++;
            dir = 3;
            newZ = map.getBlockedHeight(x + 1, y + 1, z, h);
        } else if (up && map.canWalk(x, y, x, y - 1, z, h)) {
            yOffset--;
            dir = 4;
            newZ = map.getBlockedHeight(x, y - 1, z, h);
        } else if (left && map.canWalk(x, y, x - 1, y, z, h)) {
            xOffset--;
            dir = 5;
            newZ = map.getBlockedHeight(x - 1, y, z, h);
        } else if (right && map.canWalk(x, y, x + 1, y, z, h)) {
            xOffset++;
            dir = 6;
            newZ = map.getBlockedHeight(x + 1, y, z, h);
        } else if (down && map.canWalk(x, y, x, y + 1, z, h)) {
            yOffset++;
            dir = 7;
            newZ = map.getBlockedHeight(x, y + 1, z, h);
        }

        // Handle movement.
        if (xOffset || yOffset) {
            call_moveRequest(L, dir);
        }
    }

    /**
     * Handle packet flow.
     *
     * @param post  Post service
     * @param L     Lua state
     */
    void handlePackets(PostService &post, lua_State *L)
    {
        post.process();
        Packet packet = post.recv();
        if (packet.getLength()) {
            switch (static_cast<int>(reinterpret_cast<const char *>(packet.getData())[0])) {
                case PH_LOGIN_OK:
                    call_gotLoginOKPacket(L);
                    break;
                case PH_LOGIN_FAIL:
                    call_gotLoginFailPacket(L);
                    break;
                case PH_ADD_CHARACTER:
                    call_gotAddCharacterPacket(L, reinterpret_cast<const genPacket::AddCharacterPacket *>(packet.getData()));
                    break;
                case PH_MOVE_OBJECT:
                    call_gotMoveObjectPacket(L, reinterpret_cast<const genPacket::MoveObjectPacket *>(packet.getData()));
                    break;
                case PH_LUA_REMOTE_CALL:
                    Lua::processRemoteCall(L, packet, -1);
                    break;
            }
        }
    }
}

/**
 * Main entry point for the program.
 *
 * @param argc          The number of arguments passed to the program.
 * @param argv          The arguments passed to the program.
 * @return              0 if all went OK.
 */
int main(int argc, char **argv)
{
    // Set the mode to the LOGIN.
    StateMachine::GS() = StateMachine::LOGIN;

    int playerID = 0;

    try {
        // Set up Lua
        LuaState L(luaL_newstate());
        luaL_openlibs(L);
        Lua::setPath(L);

        // Load configuration file.
        Log::GS().info("Executing init script...");
        openOdysicLib(L);
        if (luaL_loadfile(L, PKGDATADIR INIT_SCRIPT) || lua_pcall(L, 0, 0, 0)) {
            throw std::runtime_error(lua_tostring(L, -1));
        }

        // Initialize SDL.
        Log::GS().info("Initializing SDL...");
        GameObject SDL_INIT(SCREEN_WIDTH, SCREEN_HEIGHT, TITLE);

        // Initialize OpenGL.
        Log::GS().info("Initializing rendering system...");
        RenderSystem::GS().initSystem();

        // Initialize CEGUI
        Log::GS().info("Initializing CEGUI...");
        initCEGUI(L);
        setCEGUILayout(LAYOUT_LOGIN);

        // Connect to the server.
        Socket sock;
        if (sock == SOCKET_ERROR) {
            perror("Could not create socket");
            return 1;
        }
        lua_getglobal(L, CONFIG_VAL_HOST);
        lua_getglobal(L, CONFIG_VAL_PORT);
        if (!lua_isnumber(L, -1)) {
            throw std::runtime_error("The port must be a number.");
        }
        sock.connect(lua_tostring(L, -2), lua_tointeger(L, -1));
        sock.setNonblocking();
        PostService post(sock);
        Lua::registerPointer(L, LUA_CR_POST_SERVICE, reinterpret_cast<void *>(&post));

        // Create map.
        Map map(PKGDATADIR FILE_MAP);
        Lua::registerPointer(L, LUA_CR_MAP, reinterpret_cast<void *>(&map));

        // Play music
        Mix_VolumeMusic(128);

        // Load the tiles.
        loadTiles();

        // Load the mobiles.
        AnimationListManager::GS().load(PKGDATADIR FILE_CHARACTERS);

        clock_t nextTick = getTicks() + TICK_INTERVAL;
        SDL_Delay(TICK_INTERVAL);

        // Main loop.
        bool run = true;
        while (run) {
            clock_t delay = nextTick - getTicks();
            SDL_Delay(delay > 0 && delay < TICK_INTERVAL * 2 ? delay : 0);
            nextTick += TICK_INTERVAL;

            // Handle all the SDL events.
            SDL_Event e;
            while (SDL_PollEvent(&e)) {
                switch(e.type) {
                    case SDL_QUIT:
                        run = false;
                        break;
                    case SDL_MOUSEMOTION:
                        CEGUI::System::getSingleton().injectMousePosition(
                                static_cast<float>(e.motion.x),
                                static_cast<float>(e.motion.y));
                        break;
                    case SDL_MOUSEBUTTONUP:
                        handleMouseUp(e.button.button);
                        break;
                    case SDL_MOUSEBUTTONDOWN:
                        handleMouseDown(e.button.button);
                        break;
                    case SDL_KEYUP:
                        CEGUI::System::getSingleton().injectKeyUp(e.key.keysym.scancode);
                        break;
                    case SDL_KEYDOWN:
                        if (!CEGUI::System::getSingleton().injectKeyDown(SDLKeyToCEGUIKey(e.key.keysym.sym))) {
                            if (e.key.keysym.unicode != 0) {
                                CEGUI::System::getSingleton().injectChar(e.key.keysym.unicode);
                            }
                        }
                        break;
                }
            }

            lua_getglobal(L, PLAYER_ID);
            playerID = lua_tointeger(L, -1);

            // Render the screen.
            if (StateMachine::GS() == StateMachine::GAME) {
                renderMap(map, playerID);
            }
            render();

            // Handle movement.
            Uint8 *keystate = SDL_GetKeyState(NULL);
            if(keystate[SDLK_ESCAPE]) {
                // Exit if escape is pressed.
                break;
            }

            if (StateMachine::GS() == StateMachine::GAME) {
                // Tick the map and everything on it.
                map.tick();
                handleMovement(keystate, map, playerID, L);
            }
            call_tick(L);

            handlePackets(post, L);

        }
    } catch (std::exception &e) {
        Log::GS().error(e.what());
    } catch (CEGUI::Exception &e) {
        Log::GS().error(e.getMessage().c_str());
    } catch (...) {
        Log::GS().error("Unknown exception.");
    }

    return 0;
}
